#include "application.hpp"
#include "core/log.hpp"
#include "core/log_store.hpp"

#if defined(_WIN32)
#include <Windows.h>
#endif

#include <imgui_backend/gl_utils.hpp>
#if USING_SDL
#include <SDL2/SDL.h>
#include <SDL2/SDL_hints.h>
#include <SDL2/SDL_opengl.h>
#include <SDL2/SDL_version.h>
#include <SDL2/SDL_video.h>
#include <imgui_backend/imgui_impl_sdl2.h>
#elif USING_GLFW
#include <GLFW/glfw3.h>
#include <imgui_backend/imgui_impl_glfw.h>
#endif
#include <imgui_backend/imgui_impl_opengl3.h>
#include <implot.h>
#include <resource/resource.hpp>
#include <service/cfg_service.hpp>
#include <service/svc_manager.hpp>
#include "application_ui.hpp"
#include "core/assert.hpp"
#include "font_mgr.hpp"
#include "imgui_ext/ImFileDialog.h"
#include "imgui_ext/stb_image.h"
#include "plugin_mgr.hpp"
#include "utils/safe_value.hpp"

struct PiInfoApp::PiInfoAppData {
#if USING_SDL
    SDL_Window   *window;
    SDL_GLContext glContext;
#elif USING_GLFW
    GLFWwindow *window;
#endif
    UiData ui;

    // some app config here
    safe_value<bool> *vsync;
    safe_value<int>  *default_log_level;
};

#if USING_GLFW
static void glfw_error_callback (int code, const char *msg)
{
    L_ERROR ("GLFW ERROR: %s(%d).", msg, code);
}
#endif

bool PiInfoApp::initialize ()
{
    d = new PiInfoAppData;

    register_services ();

    // setup config items
    auto app_group = ServiceManager::instance ()->request_service<IConfigService> ()->group ("application", "应用程序");
    app_group->checkbox ("v_sync", "垂直同步", d->vsync, true, "开启垂直同步可以降低用电量，减少画面撕裂");
    app_group->select("default_log_level", "默认最小日志等级", d->default_log_level, [](void*,int idx)->const char*{
        static const char* log_levels[] {
            "Trace",
            "Debug",
            "Info",
            "Warning",
            "Error",
            "Fatal",
        };
        return log_levels[idx];
    }, 6, nullptr, 0, "重启后生效，日志视图默认日志等级筛选器");

    {
        // 配置日志记录器的默认筛选器
        auto level_min = d->default_log_level->read();
        auto level_mask = ((1u<<LOG_LEVEL_COUNT)-1u)<<level_min;
        log_store::instance()->set_filter_value(level_mask);
    }

    // get window size parameter
    int w, h;
    w = get_option ("width", 1024).to_int32 ();
    h = get_option ("height", 600).to_int32 ();
    // GL ES 3.0
    const char *glsl_version = "#version 300 es";

#if USING_SDL
    // Setup SDL
    if (SDL_Init (SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0)
    {
        L_FATAL ("Error: %s\n", SDL_GetError ());
        return false;
    }
    // SDL_version ver;
    // SDL_GetVersion (&ver);
    // L_DEBUG ("SDL Version: %d.%d.%d", ver.major, ver.minor, ver.patch);

    // int count = SDL_GetNumDisplayModes (0);
    // L_DEBUG ("Mode Count: %d", count);
    // SDL_DisplayMode displayMode;
    // int             err = SDL_GetCurrentDisplayMode (0, &displayMode);
    // if (err == 0)
    // {
    //     L_DEBUG ("Format: (%dx%d)%d,%d,%d,%d", displayMode.w, displayMode.h, SDL_PIXELTYPE (displayMode.format), SDL_PIXELORDER (displayMode.format), SDL_PIXELLAYOUT
    //     (displayMode.format),
    //              SDL_BITSPERPIXEL (displayMode.format));
    // }
    // L_DEBUG("All Video Modes:");
    // for (int i=0; i<count; ++i) {
    //     SDL_GetDisplayMode(0, i, &displayMode);
    //     L_DEBUG ("Format: (%dx%d)%d,%d,%d,%d", displayMode.w, displayMode.h, SDL_PIXELTYPE (displayMode.format), SDL_PIXELORDER (displayMode.format), SDL_PIXELLAYOUT
    //     (displayMode.format),
    //              SDL_BITSPERPIXEL (displayMode.format));
    // }

#if _WIN32
    SDL_GL_SetAttribute (SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
#else
    SDL_GL_SetAttribute (SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
#endif
    SDL_GL_SetAttribute (SDL_GL_CONTEXT_FLAGS, 0);
    SDL_GL_SetAttribute (SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute (SDL_GL_CONTEXT_MINOR_VERSION, 0);

    // From 2.0.18: Enable native IME.
#ifdef SDL_HINT_IME_SHOW_UI
    SDL_SetHint (SDL_HINT_IME_SHOW_UI, "1");
#endif

#ifdef SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
    // Don't disable compositing
    // Available since SDL 2.0.8
    SDL_SetHint (SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
#endif

    SDL_SetHint (SDL_HINT_TOUCH_MOUSE_EVENTS, "1");
#if defined(SDL_HINT_VITA_TOUCH_MOUSE_DEVICE)
    SDL_SetHint (SDL_HINT_VITA_TOUCH_MOUSE_DEVICE, "1");
#endif
    // Create window with graphics context
    SDL_GL_SetAttribute (SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute (SDL_GL_DEPTH_SIZE, 24);
    SDL_GL_SetAttribute (SDL_GL_STENCIL_SIZE, 8);
    auto        window_flags = (SDL_WindowFlags) (SDL_WINDOW_OPENGL | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_BORDERLESS | SDL_WINDOW_HIDDEN);
    SDL_Window *window       = SDL_CreateWindow ("PiInfoApp", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, w, h, window_flags);

    // set window icon
    {
        auto icon_data = MY_RESOURCE->get_file_content ("/assets/app_icon.png");
        if (!icon_data.empty ())
        {
            // the color format you request stb_image to output,
            // use STBI_rgb if you don't want/need the alpha channel
            int            req_format = STBI_rgb_alpha;
            int            width, height, orig_format;
            unsigned char *data = stbi_load_from_memory (icon_data.data (), icon_data.size (), &width, &height, &orig_format, req_format);
            if (data != NULL)
            {
                // Set up the pixel format color masks for RGB(A) byte arrays.
                // Only STBI_rgb (3) and STBI_rgb_alpha (4) are supported here!
                Uint32 rmask, gmask, bmask, amask;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
                int shift = (req_format == STBI_rgb) ? 8 : 0;
                rmask     = 0xff000000 >> shift;
                gmask     = 0x00ff0000 >> shift;
                bmask     = 0x0000ff00 >> shift;
                amask     = 0x000000ff >> shift;
#else  // little endian, like x86
                rmask = 0x000000ff;
                gmask = 0x0000ff00;
                bmask = 0x00ff0000;
                amask = (req_format == STBI_rgb) ? 0 : 0xff000000;
#endif

                int depth, pitch;
                if (req_format == STBI_rgb)
                {
                    depth = 24;
                    pitch = 3 * width;  // 3 bytes per pixel * pixels per row
                }
                else
                {  // STBI_rgb_alpha (RGBA)
                    depth = 32;
                    pitch = 4 * width;
                }

                SDL_Surface *surf = SDL_CreateRGBSurfaceFrom ((void *) data, width, height, depth, pitch, rmask, gmask, bmask, amask);

                if (surf == NULL)
                {
                    SDL_Log ("Creating surface failed: %s", SDL_GetError ());
                    stbi_image_free (data);
                    exit (1);
                }

                SDL_SetWindowIcon(window, surf);

                // when you don't need the surface anymore, free it..
                SDL_FreeSurface (surf);
                // .. *and* the data used by the surface!
                stbi_image_free (data);
            }
        }
    }

    L_DEBUG("Window Request Size: %dx%d", w, h);
    int ww,wh;
    SDL_GetWindowSize(window, &ww, &wh);
    L_DEBUG("SDL Window Size: %dx%d", ww, wh);
    // SDL_GetWindowSizeInPixels(window, &ww, &wh);
    // L_DEBUG("SDL Window Size(pixels): %dx%d", ww, wh);

    SDL_GLContext gl_context = SDL_GL_CreateContext (window);
    SDL_GL_MakeCurrent (window, gl_context);
    SDL_GL_SetSwapInterval (1);  // Enable vsync
    SDL_ShowCursor (SDL_DISABLE);
    gladLoadGLLoader ((GLADloadproc) &SDL_GL_GetProcAddress);
#elif USING_GLFW
    glfwSetErrorCallback (glfw_error_callback);
    PI_ASSERT (glfwInit () == GLFW_TRUE);
    glfwWindowHint (GLFW_RESIZABLE, GLFW_FALSE);
    glfwWindowHint (GLFW_DECORATED, GLFW_FALSE);
    //    glfwWindowHint (GLFW_FLOATING, GLFW_TRUE);
    glfwWindowHint (GLFW_VISIBLE, GLFW_FALSE);
    glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 0);
#if _WIN32
    glfwWindowHint (GLFW_CLIENT_API, GLFW_OPENGL_API);
#else
    glfwWindowHint (GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
#endif
    GLFWwindow *window = glfwCreateWindow (w, h, "PiInfoApp", nullptr, nullptr);
    PI_ASSERT (window != nullptr);

    // set window icon
    {
        auto icon_data = MY_RESOURCE->get_file_content ("/assets/app_icon.png");
        if (!icon_data.empty ())
        {
            int  w, h, c;
            auto data = stbi_load_from_memory (icon_data.data (), icon_data.size (), &w, &h, &c, 0);
            if (data)
            {
                GLFWimage icon {w, h, data};
                glfwSetWindowIcon (window, 1, &icon);
                stbi_image_free (data);
            }
        }
    }

#if defined(_WIN32)
    int max_width  = GetSystemMetrics (SM_CXSCREEN);
    int max_hieght = GetSystemMetrics (SM_CYSCREEN);
    glfwSetWindowMonitor (window, NULL, (max_width / 2) - (w / 2), (max_hieght / 2) - (h / 2), w, h, GLFW_DONT_CARE);
#endif

    glfwMakeContextCurrent (window);
    glfwSwapInterval (1);  // Enable vsync
    gladLoadGLLoader ((GLADloadproc) &glfwGetProcAddress);
#endif

    L_INFO ("Using OpenGL: %s", glGetString (GL_VERSION));

    // Setup Dear ImGui context
    IMGUI_CHECKVERSION ();
    ImGui::CreateContext ();
    ImPlot::CreateContext ();
    ImGuiIO &io = ImGui::GetIO ();
    (void) io;
    io.IniFilename = nullptr;
    io.LogFilename = nullptr;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;  // Enable Keyboard Controls
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;   // Enable Gamepad Controls
    io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;      // Enable Docking
    // io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;         // Enable Multi-Viewport / Platform Windows
    // io.ConfigViewportsNoAutoMerge = true;
    // io.ConfigViewportsNoTaskBarIcon = true;

    // scaling
    float scaling = get_option ("font_scale", 1.0f).to_float ();

    // Setup Dear ImGui style
    setup_style (scaling);

    // When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones.
    ImGuiStyle &style = ImGui::GetStyle ();
    if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
    {
        style.WindowRounding              = 0.0f;
        style.Colors[ImGuiCol_WindowBg].w = 1.0f;
    }

    // Setup Platform/Renderer backends
#if USING_SDL
    ImGui_ImplSDL2_InitForOpenGL (window, gl_context);
#elif USING_GLFW
    ImGui_ImplGlfw_InitForOpenGL (window, true);
#endif
    ImGui_ImplOpenGL3_Init (glsl_version);

    FontManager::instance ()->init (scaling);

    // initial ImGuiFileDialog
    ImGui::FileDialog::Instance ().CreateTexture = [] (uint8_t *data, int w, int h, char fmt) -> void * {
        GLuint tex;

        GL_CALL (glGenTextures (1, &tex));
        GL_CALL (glBindTexture (GL_TEXTURE_2D, tex));
        GL_CALL (glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
        GL_CALL (glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
        GL_CALL (glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
        GL_CALL (glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
        GL_CALL (glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, (fmt == 0) ? GL_BGRA : GL_RGBA, GL_UNSIGNED_BYTE, data));
        //        glGenerateMipmap(GL_TEXTURE_2D);
        GL_CALL (glBindTexture (GL_TEXTURE_2D, 0));

        return (void *) (uintptr_t) tex;
    };
    ImGui::FileDialog::Instance ().DeleteTexture = [] (void *tex) {
        GLuint texID = (GLuint) ((uintptr_t) tex);
        GL_CALL (glDeleteTextures (1, &texID));
    };

#if USING_SDL
    SDL_ShowWindow (window);
#elif USING_GLFW
    glfwShowWindow (window);
#endif

    d->window = window;
#if USING_SDL
    d->glContext = gl_context;
#elif USING_GLFW
    // no thing to do
#endif

    // load plugins
    PI_ASSERT (PluginManager::instance ()->init ());

    return true;
}
void PiInfoApp::cleanup ()
{
    PluginManager::instance ()->clean ();

    clear_services ();
    // Cleanup
    ImGui_ImplOpenGL3_Shutdown ();
#if USING_SDL
    ImGui_ImplSDL2_Shutdown ();
#elif USING_GLFW
    ImGui_ImplGlfw_Shutdown ();
#endif
    ImPlot::DestroyContext ();
    ImGui::DestroyContext ();

#if USING_SDL
    SDL_GL_DeleteContext (d->glContext);
    SDL_DestroyWindow (d->window);
    SDL_Quit ();
#elif USING_GLFW
    glfwDestroyWindow (d->window);
    glfwTerminate ();
#endif

    delete d;
    d = nullptr;
}

void PiInfoApp::update (pi_time_t delta, pi_time_t total)
{
    // plugin update
    PluginManager::instance()->on_tick(delta, total);

    const bool changed = d->vsync->changed ();

#if USING_SDL
    if (changed)
    {
        SDL_GL_SetSwapInterval (d->vsync->read () ? 1 : 0);
        d->vsync->handled ();
    }
    SDL_Event event;
    while (SDL_PollEvent (&event))
    {
        ImGui_ImplSDL2_ProcessEvent (&event);
        if (event.type == SDL_QUIT)
            quit ();
        if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE && event.window.windowID == SDL_GetWindowID (d->window))
            quit ();
    }
#elif USING_GLFW
    if (changed)
    {
        glfwSwapInterval (d->vsync->read () ? 1 : 0);
        d->vsync->handled ();
    }
    glfwPollEvents ();  // just poll events
#endif

    // Start the Dear ImGui frame
    ImGui_ImplOpenGL3_NewFrame ();
#if USING_SDL
    ImGui_ImplSDL2_NewFrame ();
#elif USING_GLFW
    ImGui_ImplGlfw_NewFrame ();
#endif
    ImGui::NewFrame ();

    auto &io = ImGui::GetIO ();

    // ui content
    render_ui (delta);

    // Rendering
    ImGui::Render ();
    glViewport (0, 0, (int) io.DisplaySize.x, (int) io.DisplaySize.y);
    glClearColor (0, 0, 0, 1);
    glClear (GL_COLOR_BUFFER_BIT);
    ImGui_ImplOpenGL3_RenderDrawData (ImGui::GetDrawData ());

    // Update and Render additional Platform Windows
    // (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere.
    //  For this specific demo app we could also call SDL_GL_MakeCurrent(window, gl_context) directly)
    if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
    {
#if USING_SDL
        SDL_Window   *backup_current_window  = SDL_GL_GetCurrentWindow ();
        SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext ();
#elif USING_GLFW
        GLFWwindow *backup_current_context = glfwGetCurrentContext ();
#endif
        ImGui::UpdatePlatformWindows ();
        ImGui::RenderPlatformWindowsDefault ();
#if USING_SDL
        SDL_GL_MakeCurrent (backup_current_window, backup_current_context);
#elif USING_GLFW
        glfwMakeContextCurrent (backup_current_context);
#endif
    }
#if USING_SDL
    SDL_GL_SwapWindow (d->window);
#elif USING_GLFW
    glfwSwapBuffers (d->window);
#endif
}

PiInfoApp::PiInfoApp (int argc, const char **argv) : IApplication (argc, argv) {}

UiData *PiInfoApp::ui_data ()
{
    return &d->ui;
}
